// 版权所有2011 Go作者。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package template

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
	"unicode/utf8"
)

// nextJSCtx返回上下文，该上下文确定给定的标记运行后的斜杠是否启动正则表达式而不是除法
// 运算符：/or/=。
// 
// 这假设令牌运行不包括任何字符串令牌、注释
// 令牌、正则表达式文字令牌或除法运算符。
// 
// 这在一些有效但无意义的JavaScript程序上失败，比如
// “x=++/foo/i”，它与“x++/foo/i”有很大不同，但
// 不知道它在任何已知的有用程序上失败。它基于
// JavaScript 2.0词汇语法草案，需要一个lookback标记：
// https:
func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
	s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029")
	if len(s) == 0 {
		return preceding
	}

	// 以下所有案例都属于单字节UTF-8组。
	switch c, n := s[len(s)-1], len(s); c {
	case '+', '-':
		// /++和--不是regexp的前导，而是+和-是
		// 它们被用作中缀或前缀运算符。
		start := n - 1
		// 数一数相邻的破折号或加号。
		for start > 0 && s[start-1] == c {
			start--
		}
		if (n-start)&1 == 1 {
			// 由于“--”是
			// 与“---”相同，所以到达尾随减号。
			return jsCtxRegexp
		}
		return jsCtxDivOp
	case '.':
		// 处理“42”
		if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' {
			return jsCtxDivOp
		}
		return jsCtxRegexp
	// 语言规范第7.7节中所有标点符号的后缀
	// 只包含上面未处理的结尾二进制运算符。
	case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?':
		return jsCtxRegexp
	// 语言规范
	// 第7.7节中所有标点符号的后缀，这些标点符号是上文未处理的前缀运算符。
	case '!', '~':
		return jsCtxRegexp
	// 匹配语言规范
	// 第7.7节中所有未在上面处理的开放括号中的标点符号。
	case '(', '[':
		return jsCtxRegexp
	// 匹配语言规范第7.7节中表达式开始之前的所有标点符号。
	case ':', ';', '{':
		return jsCtxRegexp
	// 注意：除了“}”之外，默认情况下会在除法运算和
	// 之前处理封闭标点符号（“}'，“]'，”）”），它可以在
	// 除法运算之前处理，就像在
	// （{valueOf:function（）{return 42}}/2 
	// 这是有效的，但在实践中，开发人员不会分割对象
	// 文字，因此，我们的启发式算法对于
	// function（）{…}/foo/。测试（x）和副作用（）；
	// “'）”标点符号可以位于正则表达式之前，如
	// if（b）/foo/。测试（x）&。。。
	// 但这比
	// （a+b）/c 
	case '}':
		return jsCtxRegexp
	default:
		// 要小得多。寻找一个标识名，看看它是否是一个关键字，
		// 可以位于正则表达式之前。
		j := n
		for j > 0 && isJSIdentPart(rune(s[j-1])) {
			j--
		}
		if regexpPrecederKeywords[string(s[j:])] {
			return jsCtxRegexp
		}
	}
	// 否则是上面没有列出的标点符号，或者
	// 是div op前面的字符串，或者是div op前面的标识符
	// 
	return jsCtxDivOp
}

// regexppreferrkeywords是一组保留的JS关键字，可以位于JS source中的
// 正则表达式之前。
var regexpPrecederKeywords = map[string]bool{
	"break":      true,
	"case":       true,
	"continue":   true,
	"delete":     true,
	"do":         true,
	"else":       true,
	"finally":    true,
	"in":         true,
	"instanceof": true,
	"return":     true,
	"throw":      true,
	"try":        true,
	"typeof":     true,
	"void":       true,
}

var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()

// indirectToJSONMarshaler在根据需要多次取消引用
// 以达到基类型（或nil）或json实现后，返回值。元帅
func indirectToJSONMarshaler(a any) any {
	// text/template现在支持将非类型的nil作为函数调用传递
	// 参数，所以我们必须支持它。否则我们会在下面惊慌失措，因为一个
	// 无法在无效的
	// 反射上调用类型或接口方法。价值见戈朗。org/issue/18716。
	if a == nil {
		return nil
	}

	v := reflect.ValueOf(a)
	for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Pointer && !v.IsNil() {
		v = v.Elem()
	}
	return v.Interface()
}

// jsValEscaper将其输入转义到一个JS表达式（第11.14节），该表达式具有
// 外部既没有副作用，也没有自由变量（NaN，无穷大）。
func jsValEscaper(args ...any) string {
	var a any
	if len(args) == 1 {
		a = indirectToJSONMarshaler(args[0])
		switch t := a.(type) {
		case JS:
			return string(t)
		case JSStr:
			// 待办事项：规范化引号。
			return `"` + string(t) + `"`
		case json.Marshaler:
			// 不要将其视为一名特约记者。
		case fmt.Stringer:
			a = t.String()
		}
	} else {
		for i, arg := range args {
			args[i] = indirectToJSONMarshaler(arg)
		}
		a = fmt.Sprint(args...)
	}
	// TODO:在调用Marshal之前检测循环，Marshal在
	// 循环数据上无限循环。这可能是不可接受的DoS风险。
	b, err := json.Marshal(a)
	if err != nil {
		// 在注释前放一个空格，这样如果它与
		// 除法运算符对齐，它就不会变成行注释：
		// x/{y}}
		// 变成
		// x 
		// 第二行错误消息*/null 
		return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /"))
	}

	// “<！”、“-->”、“<！[CDATA[”、“]]>”或“</script”
	// 以防自定义封送员生成包含这些内容的输出。
	// 注意：不要使用\x转义来保存字节，因为它与JSON不兼容，并且此转义器
	// 支持ld+JSON内容类型。
	if len(b) == 0 {
		// In，`x=y/{}*z`a json。生成“”的封送拆收器应该
		// 不会导致输出`x=y/*z`。
		return " null "
	}
	first, _ := utf8.DecodeRune(b)
	last, _ := utf8.DecodeLastRune(b)
	var buf strings.Builder
	// 防止标识名和数字文字进入
	// 关键字：in、instanceof、typeof、void 
	pad := isJSIdentPart(first) || isJSIdentPart(last)
	if pad {
		buf.WriteByte(' ')
	}
	written := 0
	// 确保json。Marshal转义代码点U+2028和U+2029 
	// 因此它属于JSON的子集，即有效JS。
	for i := 0; i < len(b); {
		rune, n := utf8.DecodeRune(b[i:])
		repl := ""
		if rune == 0x2028 {
			repl = `\u2028`
		} else if rune == 0x2029 {
			repl = `\u2029`
		}
		if repl != "" {
			buf.Write(b[written:i])
			buf.WriteString(repl)
			written = i + n
		}
		i += n
	}
	if buf.Len() != 0 {
		buf.Write(b[written:])
		if pad {
			buf.WriteByte(' ')
		}
		return buf.String()
	}
	return string(b)
}

// jsStrEscaper生成一个字符串，该字符串可以包含在
// JavaScript源代码、嵌入HTML5<script>元素的JavaScript、
// 或HTML5事件处理程序属性（如onclick）的引号之间。
func jsStrEscaper(args ...any) string {
	s, t := stringify(args...)
	if t == contentTypeJSStr {
		return replace(s, jsStrNormReplacementTable)
	}
	return replace(s, jsStrReplacementTable)
}

// jsRegexpEscaper的行为与jsStrEscaper类似，但逃避了正则表达式
// specials，因此当包含在正则
// expression literal中时，结果将按字面意义处理/foo{.X}}bar/匹配字符串“foo”后接
// 字符串{.X}后接字符串“bar”的文本。
func jsRegexpEscaper(args ...any) string {
	s, _ := stringify(args...)
	s = replace(s, jsRegexpReplacementTable)
	if s == "" {
		// {{.X}}/在使用时不应产生行注释。X==“”。
		return "(?:)"
	}
	return s
}

// replace用replacementTable[r]替换s中的每个符文r，前提是
// r<len（replacementTable）。如果replacementTable[r]是空字符串，则
// 不进行替换。
// 它还将符文U+2028和U+2029替换为原始字符串“\u2028”和
// /`\u2029`。
func replace(s string, replacementTable []string) string {
	var b strings.Builder
	r, w, written := rune(0), 0, 0
	for i := 0; i < len(s); i += w {
		// 参见htmlEscaper中的评论。
		r, w = utf8.DecodeRuneInString(s[i:])
		var repl string
		switch {
		case int(r) < len(lowUnicodeReplacementTable):
			repl = lowUnicodeReplacementTable[r]
		case int(r) < len(replacementTable) && replacementTable[r] != "":
			repl = replacementTable[r]
		case r == '\u2028':
			repl = `\u2028`
		case r == '\u2029':
			repl = `\u2029`
		default:
			continue
		}
		if written == 0 {
			b.Grow(len(s))
		}
		b.WriteString(s[written:i])
		b.WriteString(repl)
		written = i + w
	}
	if written == 0 {
		return s
	}
	b.WriteString(s[written:])
	return b.String()
}

var lowUnicodeReplacementTable = []string{
	0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
	'\a': `\u0007`,
	'\b': `\u0008`,
	'\t': `\t`,
	'\n': `\n`,
	'\v': `\u000b`, // /“\v”==“v”在IE 6上。
	'\f': `\f`,
	'\r': `\r`,
	0xe:  `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
	0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
	0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
}

var jsStrReplacementTable = []string{
	0:    `\u0000`,
	'\t': `\t`,
	'\n': `\n`,
	'\v': `\u000b`, // /“\v”==“v”在IE 6上。
	'\f': `\f`,
	'\r': `\r`,
	// 将HTML特殊值编码为十六进制，这样输出就可以嵌入到HTML属性中，而无需进一步编码。
	'"':  `\u0022`,
	'&':  `\u0026`,
	'\'': `\u0027`,
	'+':  `\u002b`,
	'/':  `\/`,
	'<':  `\u003c`,
	'>':  `\u003e`,
	'\\': `\\`,
}

// JSSTRNORMREPLACENTABLE类似于JSSTRREPLACENTABLE，但不
// 过度编码现有的转义，因为此表没有“\”项。
var jsStrNormReplacementTable = []string{
	0:    `\u0000`,
	'\t': `\t`,
	'\n': `\n`,
	'\v': `\u000b`, // /“\v”==“v”在IE 6上。
	'\f': `\f`,
	'\r': `\r`,
	// 将HTML特殊值编码为十六进制，这样输出就可以嵌入到HTML属性中，而无需进一步编码。
	'"':  `\u0022`,
	'&':  `\u0026`,
	'\'': `\u0027`,
	'+':  `\u002b`,
	'/':  `\/`,
	'<':  `\u003c`,
	'>':  `\u003e`,
}
var jsRegexpReplacementTable = []string{
	0:    `\u0000`,
	'\t': `\t`,
	'\n': `\n`,
	'\v': `\u000b`, // /“\v”==“v”在IE 6上。
	'\f': `\f`,
	'\r': `\r`,
	// 将HTML特殊值编码为十六进制，这样就可以将输出嵌入到HTML属性中，而无需进一步编码。
	'"':  `\u0022`,
	'$':  `\$`,
	'&':  `\u0026`,
	'\'': `\u0027`,
	'(':  `\(`,
	')':  `\)`,
	'*':  `\*`,
	'+':  `\u002b`,
	'-':  `\-`,
	'.':  `\.`,
	'/':  `\/`,
	'<':  `\u003c`,
	'>':  `\u003e`,
	'?':  `\?`,
	'[':  `\[`,
	'\\': `\\`,
	']':  `\]`,
	'^':  `\^`,
	'{':  `\{`,
	'|':  `\|`,
	'}':  `\}`,
}

// isjsidentipart报告给定符文是否为JS标识符部分。
// 它不会处理所有非拉丁字母、连接符和组合标记，但它会处理数字文本或关键字中可能出现的每个代码点。
func isJSIdentPart(r rune) bool {
	switch {
	case r == '$':
		return true
	case '0' <= r && r <= '9':
		return true
	case 'A' <= r && r <= 'Z':
		return true
	case r == '_':
		return true
	case 'a' <= r && r <= 'z':
		return true
	}
	return false
}

// isJSType报告给定的MIME类型是否应被视为JavaScript。
// 
// 用于确定带有type属性的脚本标记是否为javascript容器。
func isJSType(mimeType string) bool {
	// per 
	// https:
	// https:
	// https:
	// https:
	// 放弃参数
	mimeType, _, _ = strings.Cut(mimeType, ";")
	mimeType = strings.ToLower(mimeType)
	mimeType = strings.TrimSpace(mimeType)
	switch mimeType {
	case
		"application/ecmascript",
		"application/javascript",
		"application/json",
		"application/ld+json",
		"application/x-ecmascript",
		"application/x-javascript",
		"module",
		"text/ecmascript",
		"text/javascript",
		"text/javascript1.0",
		"text/javascript1.1",
		"text/javascript1.2",
		"text/javascript1.3",
		"text/javascript1.4",
		"text/javascript1.5",
		"text/jscript",
		"text/livescript",
		"text/x-ecmascript",
		"text/x-javascript":
		return true
	default:
		return false
	}
}
